dbgp = {};

dbgp.Session = function(connection, oninit, onerror) {
  this.connection = connection;
  this.queryInterface = new dbgp.QueryInterface(this);
  this.transactions = {};
  this.oninit = oninit;
  var session = this;
  var buffer = null;
  this.connection.listen(
    function(data) {
      var packets;
      try {
        if (buffer) {
          data = buffer + data;
          buffer = null;
        }
        packets = session.parsePackets(data);
      } catch (err) {
        // an error can occur, if the data is partial
        // this is to be expected with large data-transfers
        buffer = data;
        packets = null;
      }
      if (packets) {
        spectator.debug("Received packet(s): " + packets.length);
        try {
          mapcar(
            bind("dispatchPacket", session),
            packets);
        } catch (err) {
          onerror(err, session);
        }
      }
    }
  );
};

dbgp.Session.errorMessages = {
  1 : 'parse error in command',
  2 : 'duplicate arguments in command',
  3 : 'invalid options (ie, missing a required option)',
  4 : 'Unimplemented command',
  5 : 'Command not available (Is used for async commands. For instance if the engine is in state "run" than only "break" and "status" are available).',
  100 : 'can not open file (as a reply to a "source" command if the requested source file can\'t be opened)',
  101 : 'stream redirect failed',
  200 : 'breakpoint could not be set (for some reason the breakpoint could not be set due to problems registering it)',
  201 : 'breakpoint type not supported (for example I don\'t support \'watch\' yet and thus return this error)',
  202 : 'invalid breakpoint (the IDE tried to set a breakpoint on a line that does not exist in the file (ie "line 0" or lines past the end of the file)',
  203 : 'no code on breakpoint line (the IDE tried to set a breakpoint on a line which does not have any executable code. The despectacle engine is NOT required to return this type if it is impossible to determine if there is code on a given location. (For example, in the PHP despectacle backend this will only be returned in some special cases where the current scope falls into the scope of the breakpoint to be set)).',
  204 : 'Invalid breakpoint state (using an unsupported breakpoint state was attempted)',
  205 : 'No such breakpoint (used in breakpoint_get etc. to show that there is no breakpoint with the given ID)',
  206 : 'Error evaluating code (use from eval() (or perhaps property_get for a full name get))',
  207 : 'Invalid expression (the expression used for a non-eval() was invalid)',
  300 : 'Can not get property (when the requested property to get did not exist, this is NOT used for an existing but uninitialized property, which just gets the type "uninitialised" (See: PreferredTypeNames)).',
  301 : 'Stack depth invalid (the -d stack depth parameter did not exist (ie, there were less stack elements than the number requested) or the parameter was < 0)',
  302 : 'Context invalid (an non existing context was requested)',
  900 : 'Encoding not supported',
  998 : 'An internal exception in the despectacle occurred',
  999 : 'Unknown error',
};

dbgp.Session.prototype.dispatchPacket = function(packet) {
  if (packet.init) {
    this.oninit(packet.init, this);
    return;
  }
  if (packet.response) {
    // todo ... some duplicate code here ... do clean it up
    if (packet.response.error) {
      var error = new Error(dbgp.Session.errorMessages[packet.response.error.code]);
      error.code = packet.response.error.code;
      error.apperr = packet.response.error.apperr;
      var deferred = this.transactions[packet.response.transaction_id];
      this.transactions[packet.response.transaction_id] = null;
      deferred.errback(error);
      return;
    }
    if (packet.response.transaction_id) {
      var deferred = this.transactions[packet.response.transaction_id];
      this.transactions[packet.response.transaction_id] = null;
      if (packet.response.encoding && packet.response._value_) {
        deferred.callback(packet.response._value_);
        return;
      }
      deferred.callback(packet.response);
      return;
    }
    spectator.debug(packet.response);
  }
  if (packet.stream) {
    if (packet.stream.type != "stdout" && packet.stream.type != "stderr") {
      throw new Error("Unknown stream type '" + packet.stream.type + "'");
    }
    var deferred = this.transactions[packet.response.transaction_id];
    this.transactions[packet.stream.transaction_id] = null;
    deferred.callback(packet.stream._value_);
    return;
  }
  throw new Error("Unrecognised packet type");
};

dbgp.Session.prototype.sendCommand = function(command, args, data) {
  if (!args) {
    args = [];
  }
  var id = (new Date()).getTime();
  args['i'] = id;
  var compiled = [command];
  for (var key in args) {
    compiled.push('-' + key + ' "' + (args[key] + '').replace('"', '\\"') + '"');
  }
  if (data) {
    compiled.push(btoa(data));
  }
  var message = compiled.join(" ");
  spectator.debug("Sending message: " + message);
  this.connection.write(message + "\0");
  this.transactions[id] = new Deferred();
  return this.transactions[id];
};

dbgp.Session.prototype.close = function() {
  this.connection.close();
};

dbgp.Session.prototype.parsePackets = function(raw) {
  var packets = [];
  while (raw.length > 0) {
    var parts = raw.match(/^[0-9]+/);
    if (!parts) {
      throw new Error("Unable to tokenize raw-packet");
    }
    var size = parseInt(parts[0]) + 1;
    if (raw.length < parts[0].length + size) {
      throw new Error("Packet malformed");
    }
    var content = raw.substr(parts[0].length + 1, size);
    packets.push(
      xml.objectify(
        xml.parseFromString(content)));
    raw = raw.substr(parts[0].length + 1 + size);
  }
  return packets;
};

dbgp.QueryInterface = function(session) {
  this.session = session;
};

dbgp.QueryInterface.prototype.close = function() {
  this.session.close();
};

dbgp.QueryInterface.prototype.getStatus = function() {
  return this.session.sendCommand("status");
};

dbgp.QueryInterface.prototype.getFeature = function(featureName) {
  return this.session.sendCommand("feature_get", { n: featureName });
};

dbgp.QueryInterface.prototype.setFeature = function(featureName, value) {
  return this.session.sendCommand("feature_set", { n: featureName, v: value });
};

dbgp.QueryInterface.prototype.run = function() {
  return this.session.sendCommand("run");
};

dbgp.QueryInterface.prototype.stepInto = function() {
  return this.session.sendCommand("step_into");
};

dbgp.QueryInterface.prototype.stepOver = function() {
  return this.session.sendCommand("step_over");
};

dbgp.QueryInterface.prototype.stepOut = function() {
  return this.session.sendCommand("step_out");
};

dbgp.QueryInterface.prototype.stop = function() {
  return this.session.sendCommand("stop");
};

dbgp.QueryInterface.prototype.detach = function() {
  return this.session.sendCommand("detach");
};

dbgp.QueryInterface.prototype.breakpointSetLine = function(filename, lineno) {
  return this.session.sendCommand("breakpoint_set", {t: "line", f: filename, n: lineno});
};

dbgp.QueryInterface.prototype.breakpointRemove = function(id) {
  return this.session.sendCommand("breakpoint_remove", {d: id});
};

dbgp.QueryInterface.prototype.stackDepth = function() {
  var inner = this.session.sendCommand("stack_depth");
  var outer = new Deferred();
  inner.addErrback(bind('errback', outer));
  inner.addCallback(
    function(response) {
      outer.callback(response.depth);
    }
  );
  return outer;
};

dbgp.QueryInterface.prototype.source = function(file, begin, end) {
  var args = {};
  if (typeof(file) != "undefined") {
    args['f'] = file;
  }
  if (typeof(begin) != "undefined") {
    args['b'] = begin;
  }
  if (typeof(end) != "undefined") {
    args['e'] = end;
  }
  return this.session.sendCommand("source", args);
};

dbgp.QueryInterface.prototype.contextNames = function(depth) {
  var args = {};
  if (typeof(depth) != "undefined") {
    args['d'] = depth;
  }
  var inner = this.session.sendCommand("context_names", args);
  var outer = new Deferred();
  inner.addErrback(bind('errback', outer));
  inner.addCallback(
    function(response) {
      if (response.context.name) {
        outer.callback([response.context]);
      } else {
        outer.callback(response.context);
      }
    }
  );
  return outer;
};

dbgp.QueryInterface.prototype.stackGet = function(depth) {
  var args = {};
  if (typeof(depth) != "undefined") {
    args['d'] = depth;
  }
  var inner = this.session.sendCommand("stack_get", args);
  var outer = new Deferred();
  inner.addErrback(bind('errback', outer));
  inner.addCallback(
    function(response) {
      outer.callback(response.stack);
    }
  );
  return outer;
};

dbgp.QueryInterface.prototype.contextGet = function(depth, context) {
  var args = {};
  if (typeof(depth) != "undefined") {
    args['d'] = depth;
  }
  if (typeof(context) != "undefined") {
    args['c'] = context;
  }
  // todo possibly do some processing before sending to the caller
  return this.session.sendCommand("context_get", args);
};

/** Retrieves all stacks */
dbgp.QueryInterface.prototype.getCallstack = function() {
  var d = new Deferred();
  var qi = this;
  this.stackDepth()
  .addCallback(
    function(depth) {
      var j = 0;
      var fullstack = [];
      for (var i=0; i < depth; i++) {
        qi.stackGet(i)
          .addCallback(
            function(stack) {
              fullstack[stack.level] = stack;
              j++;
              if (j == depth) {
                d.callback(fullstack);
              }
            }
          )
          .addErrback(bind("errback", d));
      }
    }
  )
  .addErrback(bind("errback", d));
  return d;
};

/** Retrieves all contexts for stack */
dbgp.QueryInterface.prototype.getContext = function(depth) {
  var d = new Deferred();
  var qi = this;
  this.contextNames(depth)
  .addCallback(
    function(names) {
      var j = names.length;
      var ids = [];
      var names_indexed = {};
      mapcar(
        function(name) {
          ids.push(name.id);
          names_indexed[name.id] = name.name;
        },
        names);
      var contexts = {};
      for (var i=0; i < names.length; i++) {
        qi.contextGet(depth, names[i].id)
          .addCallback(
            function(response) {
              response.name = names_indexed[response.context];
              contexts[response.context] = response;
              j--;
              if (j == 0) {
                var ordered = [];
                for (var i=0; i < ids.length; i++) {
                  ordered.push(contexts[ids[i]]);
                }
                d.callback(ordered);
              }
            }
          )
          .addErrback(bind("errback", d));
      }
    }
  )
  .addErrback(bind("errback", d));
  return d;
};
